import httpEnum, { httpPriority, httpStatus } from "../enum/http.enum"

declare global {
  const XDomainRequest: any
}
export class RequestQueue {
  totalBytesDownloaded: number
  retries: number
  concurrency: number
  queue: Request[]
  updateTimeout: number | null | undefined
  constructor(setting?: { retries?: number; concurrency?: number }) {
    const retries = setting?.retries
    const concurrency = setting?.concurrency
    this.totalBytesDownloaded = 0
    this.queue = []
    this.retries = retries || 0
    this.concurrency = concurrency || 6
  }
  get doNotRetryStatusCodes() {
    return {
      400: !0,
      401: !0,
      403: !0,
      404: !0,
      405: !0,
      406: !0,
      410: !0,
      411: !0,
      414: !0,
      415: !0,
      421: !0,
      431: !0,
      451: !0
    }
  }
  get(url: string, setting?: Setting) {
    return this.request("GET", url, setting)
  }
  head(url: string, setting?: Setting) {
    return this.request("HEAD", url, setting)
  }
  options(url: string, setting?: Setting) {
    return this.request("OPTIONS", url, setting)
  }
  post(url: string, setting?: Setting) {
    return this.request("POST", url, setting)
  }
  put(url: string, setting?: Setting) {
    return this.request("PUT", url, setting)
  }
  patch(url: string, setting?: Setting) {
    return this.request("PATCH", url, setting)
  }
  delete(url: string, setting?: Setting) {
    return this.request("DELETE", url, setting)
  }
  request(method: keyof typeof httpEnum, url: string, setting?: Setting) {
    const req = new Request(method, url, setting)
    this.enqueue(req)
    return req.promise
  }
  update() {
    let req: Request | null
    for (; (req = this.getNextPendingRequest()); ) this.sendRequest(req)
    for (; (req = this.getNextOverflowingGet()); ) req.abort(), (req.status = httpStatus.PENDING)
    this.updateTimeout = null
  }
  enqueue(req: Request) {
    let i = 0
    for (i = 0; i < this.queue.length; i++) {
      if (this.queue[i].priority < req.priority) break
    }
    this.queue.splice(i, 0, req)
    this.updateTimeout ||
      (this.updateTimeout = window.setTimeout(() => {
        this.update()
      }, 1))
  }
  dequeue(req: Request) {
    const index = this.queue.indexOf(req)
    if (-1 === index) throw new Error("Can't dequeue request not in queue")
    this.queue.splice(index, 1)
    this.update()
  }
  getNextPendingRequest() {
    for (var e = 0; e < this.queue.length && e < this.concurrency; e++) {
      var req = this.queue[e]
      if (req.status === httpStatus.PENDING) return req
    }
    return null
  }
  getNextOverflowingGet() {
    for (var e = this.concurrency; e < this.queue.length; e++) {
      var req = this.queue[e]
      if (req.status === httpStatus.SENDING && req.priority !== httpPriority.HIGHEST && "GET" === req.method) return req
    }
    return null
  }
  shouldRetryStatusCode(code: number) {
    return !this.doNotRetryStatusCodes[code]
  }
  sendRequest(req: Request) {
    req.status = httpStatus.SENDING
    req
      .send()
      .then(i => {
        req.status = httpStatus.DONE
        this.dequeue(req)
        req.contentLength && req.contentLength > 0 && (this.totalBytesDownloaded += Number(req.contentLength))
        req.onDone!(i)
      })
      .catch(i => {
        var s = req.maxRetries || this.retries,
          r = req.sendAttempts < s
        if ("object" == typeof i) {
          var o = i.status_code || 0
          r = r && this.shouldRetryStatusCode(o)
        }
        r
          ? ((req.status = httpStatus.PENDING), this.update(), console.warn("Retried " + req.url), console.warn(i))
          : ((req.status = httpStatus.FAILED), this.dequeue(req), console.warn("Failed " + req.url), req.onFail!(i))
      })
  }
}
interface Setting {
  cache?: boolean
  auth?: string | null
  withCredentials?: boolean
  priority?: number
  responseType?: string
  body?: any
  headers?: any
  maxRetries?: number | null
  onProgress?: Function
}
export class Request {
  sendAttempts: number
  status: number
  contentLength: number
  url: string
  method: keyof typeof httpEnum
  auth: string | null
  withCredentials: boolean
  priority: number
  responseType: string | null
  body: any
  headers: any
  maxRetries: number | null
  onProgress: any
  promise: Promise<any>
  onDone: Function | undefined
  onFail: Function | undefined
  xhr: XMLHttpRequest | undefined
  constructor(method: keyof typeof httpEnum, url: string, setting?: Setting) {
    const config: Partial<Setting> = setting || {}
    this.sendAttempts = 0
    this.status = httpStatus.PENDING
    this.contentLength = 0
    this.url = url
    this.method = method
    this.auth = config.auth || null
    this.withCredentials = config.withCredentials || !1
    this.priority = config.priority || httpPriority.MEDIUM
    this.responseType = config.responseType || null
    this.body = config.body || null
    this.headers = config.headers || {}
    this.maxRetries = config.maxRetries || null
    this.onProgress = config.onProgress
    this.promise = new Promise((resolve, reject) => {
      this.onDone = resolve
      this.onFail = reject
    })
  }
  send() {
    if ("undefined" != typeof XMLHttpRequest) (this.xhr = new XMLHttpRequest()).withCredentials = this.withCredentials
    else {
      if ("undefined" == typeof XDomainRequest) throw new Error("No XMLHTTPRequest or XDomainRequest... are you trying to run me in node? :(")
      this.xhr = new XDomainRequest()
    }
    this.xhr!.open(this.method, this.url, !0)
    if (this.responseType)
      if ("arraybuffer" === this.responseType || "text" === this.responseType || "json" === this.responseType || "blob" === this.responseType)
        this.xhr!.responseType = this.responseType
      else {
        if ("image" !== this.responseType) throw new Error('reponseType can only be one of "arraybuffer", "text", "json", "blob", "image"')
        this.xhr!.responseType = "blob"
      }
    for (var i in ("json" === this.responseType && this.xhr!.setRequestHeader("Accept", "application/json"),
    this.auth && "string" == typeof this.auth && this.xhr!.setRequestHeader("Authorization", this.auth),
    this.headers))
      this.xhr!.setRequestHeader(i, this.headers[i])
    this.body &&
      "object" == typeof this.body &&
      (this.body instanceof FormData || ((this.body = JSON.stringify(this.body)), this.xhr!.setRequestHeader("Content-Type", "application/json")))
    this.onProgress && (this.xhr!.onprogress = this.onProgress)
    return new Promise((resolve, reject) => {
      this.xhr!.onreadystatechange = async () => {
        if (4 === this.xhr!.readyState) {
          if (200 === this.xhr!.status || 201 === this.xhr!.status || 204 === this.xhr!.status)
            return this.parseResponse(this.xhr!).then(e => {
              resolve(e)
            })
          if (0 !== this.xhr!.status)
            return this.parseResponse(this.xhr!)
              .then(e => {
                reject(Object.assign({ status_code: this.xhr!.status }, e))
              })
              .catch(() => {
                reject({ status_code: this.xhr!.status })
              })
        }
      }
      this.xhr!.onerror = e => {
        reject(e)
      }
      this.xhr!.send(this.body)
      this.sendAttempts++
    })
  }
  parseResponse(e: XMLHttpRequest) {
    return new Promise((resolve, reject) => {
      try {
        const response: Response = e.response
        this.contentLength = Number(e.getResponseHeader("Content-Length"))
        if ("json" === this.responseType && "object" != typeof response && this.contentLength) resolve(JSON.parse(e.responseText))
        else if ((200 !== e.status && 201 !== e.status && 204 !== e.status) || "image" !== this.responseType) resolve(response)
        else {
          const url = URL.createObjectURL(response as any)
          const imgage = new Image()
          imgage.src = url
          imgage.crossOrigin = "Anonymous"
          imgage.onload = () => {
            URL.revokeObjectURL(url)
            resolve(imgage)
          }
        }
      } catch (e) {
        reject({ error: "Payload was not valid JSON" })
      }
    })
  }
  abort() {
    if (!this.xhr) throw new Error("Cannot abort unsent Request")
    this.xhr.abort()
  }
}
export default {
  RequestQueue,
  Request
}
